// SPDX-FileCopyrightText: Adam Evyčędo
//
// SPDX-License-Identifier: GPL-3.0-or-later

package xyz.apiote.bimba.czwek.departures

import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_UNDEFINED
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.widget.TooltipCompat
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textview.MaterialTextView
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.CustomZoomButtonsController
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.TilesOverlay
import org.osmdroid.views.overlay.gestures.RotationGestureOverlay
import xyz.apiote.bimba.czwek.R
import xyz.apiote.bimba.czwek.dpToPixelI
import xyz.apiote.bimba.czwek.repo.Alert
import xyz.apiote.bimba.czwek.repo.CongestionLevel
import xyz.apiote.bimba.czwek.repo.Event
import xyz.apiote.bimba.czwek.repo.EventItem
import xyz.apiote.bimba.czwek.repo.OccupancyStatus
import xyz.apiote.bimba.czwek.repo.Vehicle
import xyz.apiote.bimba.czwek.units.UnitSystem
import java.time.ZoneId
import java.time.ZonedDateTime

class BimbaDepartureViewHolder(itemView: View) : ViewHolder(itemView) {
	val root: View = itemView.findViewById(R.id.departure)
	val lineIcon: ImageView = itemView.findViewById(R.id.line_icon)
	val arrivalTime: TextView = itemView.findViewById(R.id.arrival_time)
	val arrivalStatus: TextView = itemView.findViewById(R.id.arrival_status)
	val departureTime: TextView = itemView.findViewById(R.id.departure_time)
	val departureStatus: TextView = itemView.findViewById(R.id.departure_status)
	val lineName: TextView = itemView.findViewById(R.id.departure_line)
	val headsign: TextView = itemView.findViewById(R.id.departure_headsign)
	val eventStatus: ImageView = itemView.findViewById(R.id.event_status)

	companion object {
		fun bind(
			event: Event,
			holder: BimbaDepartureViewHolder?,
			context: Context?,
			showAsTime: Boolean,
			onClickListener: (Event) -> Unit,
			showingTerminusArrivals: String
		) {
			holder?.root?.setOnClickListener {
				onClickListener(event)
			}
			holder?.lineIcon?.setImageDrawable(event.vehicle.Line.icon(context!!))
			holder?.lineIcon?.contentDescription = event.vehicle.Line.kind.name
			holder?.lineName?.text = event.vehicle.Line.name
			holder?.headsign?.text = if (event.vehicle.Headsign.isNotBlank()) {
				context?.getString(R.string.departure_headsign, event.vehicle.Headsign)
			} else {
				""
			}
			holder?.headsign?.contentDescription = if (event.vehicle.Headsign.isNotBlank()) {
				context?.getString(
					R.string.departure_headsign_content_description,
					event.vehicle.Headsign
				)
			} else {
				""
			}

			when {
				event.isRealtime -> {
					holder?.eventStatus?.let {
						it.contentDescription =
							context?.getString(R.string.realtime_content_description)
						it.setImageResource(R.drawable.radar)
						TooltipCompat.setTooltipText(
							it,
							context?.getString(R.string.realtime_content_description)
						)
						// TODO all at the same time with setCurrentFraction based on wall clock
						/*ObjectAnimator.ofPropertyValuesHolder(it, PropertyValuesHolder.ofFloat("alpha", 0.0f))
							.apply {
								setDuration(1000)
								repeatCount = ObjectAnimator.INFINITE
								repeatMode = ObjectAnimator.REVERSE
							}
							.start()*/
					}
				}

				event.exact -> {
					// FIXME clear animation
					holder?.eventStatus?.let {
						it.setImageResource(R.drawable.calendar)
						it.contentDescription =
							context?.getString(R.string.schedule_content_description)
						TooltipCompat.setTooltipText(
							it,
							context?.getString(R.string.schedule_content_description)
						)
					}
				}
			}

			val statusTexts = event.statusText(context, showAsTime)
			if (event.arrivalTime == event.departureTime) {
				if (!event.exact) {
					holder?.arrivalStatus?.apply {
						visibility = View.VISIBLE
						text = context?.getString(R.string.approximately)
					}
				} else {
					holder?.arrivalStatus?.visibility = View.INVISIBLE
				}
				holder?.arrivalTime?.apply {
					text = statusTexts.second
					visibility = View.VISIBLE
				}
				holder?.departureStatus?.visibility = View.GONE
				holder?.departureTime?.visibility = View.GONE
			} else {
				if (statusTexts.first != null) {
					holder?.arrivalTime?.visibility = View.VISIBLE
					holder?.arrivalTime?.text = statusTexts.first
					holder?.arrivalStatus?.visibility = View.VISIBLE
					holder?.arrivalStatus?.text = if (!event.exact) {
						context?.getString(R.string.arrival_approximate)
					} else {
						context?.getString(R.string.arrival)
					}
				} else {
					holder?.arrivalTime?.visibility = View.GONE
					holder?.arrivalStatus?.visibility = View.GONE
				}
				if (statusTexts.second != null) {
					holder?.departureTime?.visibility = View.VISIBLE
					holder?.departureTime?.text = statusTexts.second
					holder?.departureStatus?.visibility = View.VISIBLE
					holder?.departureStatus?.text = if (!event.exact) {
						context?.getString(R.string.departure_approximate)
					} else {
						context?.getString(R.string.departure)
					}
				} else {
					holder?.departureTime?.visibility = View.GONE
					holder?.departureStatus?.visibility = View.GONE
				}
			}
			holder?.root?.alpha =
				if (event.terminusArrival && showingTerminusArrivals == BimbaDeparturesAdapter.TERMINUS_ARRIVAL_GREY_OUT) {
					.5f
				} else {
					1f
				}
		}
	}
}

class BimbaAlertViewHolder(itemView: View) : ViewHolder(itemView) {
	val root: View = itemView.findViewById(R.id.alerts)
	val text: TextView = itemView.findViewById(R.id.alerts_text)
	val moreButton: Button = itemView.findViewById(R.id.more_button)

	companion object {
		fun bind(
			alerts: List<Alert>,
			holder: BimbaAlertViewHolder?,
			context: Context?
		) {
			val alertDescriptions = alerts.map { it.description }.filter { it != "" }
				.joinToString(separator = "\n")
			holder?.moreButton?.setOnClickListener {
				MaterialAlertDialogBuilder(context!!)
					.setTitle(R.string.alerts)
					.setPositiveButton(R.string.ok) { _, _ -> }
					.setMessage(alertDescriptions)
					.show()
			}
			holder?.moreButton?.visibility = if (alertDescriptions == "") View.GONE else View.VISIBLE
			holder?.text?.text = alerts.map {
				it.header.ifEmpty {
					context!!.getString(R.string.alert_header)
				}
			}.toSet().joinToString(separator = "\n")
		}
	}
}

class BimbaDeparturesAdapter(
	private val inflater: LayoutInflater,
	private val context: Context?,
	private var items: List<EventItem>,
	private val onClickListener: ((Event) -> Unit),
) :
	RecyclerView.Adapter<ViewHolder>() {

	companion object {
		const val ALERT_ITEM_ID = "alert"

		// TODO to enum
		const val TERMINUS_ARRIVAL_SHOWING_KEY = "terminus_arrival_showing"
		const val TERMINUS_ARRIVAL_GREY_OUT = "grey_out"
		const val TERMINUS_ARRIVAL_HIDE = "hide"
		const val TERMINUS_ARRIVAL_SHOW = "show"
	}

	var showingTerminusArrivals: String = context?.let {
		PreferenceManager.getDefaultSharedPreferences(
			it
		).getString(TERMINUS_ARRIVAL_SHOWING_KEY, TERMINUS_ARRIVAL_GREY_OUT)
	}
		?: TERMINUS_ARRIVAL_GREY_OUT

	var lastUpdate: ZonedDateTime =
		ZonedDateTime.of(0, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault())
		private set
	private var showAsTime: Boolean = false

	inner class DiffUtilCallback(
		private val oldDepartures: List<EventItem>,
		private val newDepartures: List<EventItem>,
		private val showAsTimeChanged: Boolean,
	) : DiffUtil.Callback() {
		override fun getOldListSize() = oldDepartures.size

		override fun getNewListSize() = newDepartures.size

		override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
			(oldDepartures[oldItemPosition].event?.id
				?: ALERT_ITEM_ID) == (newDepartures[newItemPosition].event?.id ?: ALERT_ITEM_ID)

		override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
			val oldDeparture = oldDepartures[oldItemPosition]
			val newDeparture = newDepartures[newItemPosition]
			return if (oldDeparture.event != null && newDeparture.event != null) {
				!oldDeparture.event.terminusArrival &&
					oldDeparture.event.terminusArrival == newDeparture.event.terminusArrival &&
					oldDeparture.event.exact == newDeparture.event.exact &&
					oldDeparture.event.vehicle.Line == newDeparture.event.vehicle.Line &&
					oldDeparture.event.vehicle.Headsign == newDeparture.event.vehicle.Headsign &&
					oldDeparture.event.statusText(
						context,
						false,
						lastUpdate
					) == newDeparture.event.statusText(context, false) && !showAsTimeChanged
			} else if (oldDeparture.alert.isNotEmpty() && newDeparture.alert.isEmpty()) {
				oldDeparture.alert == newDeparture.alert
			} else {
				false
			}
		}
	}

	private var departuresPositions: MutableMap<String, Int> = HashMap()

	init {
		items.forEachIndexed { i, departure ->
			departuresPositions[departure.event?.id ?: ALERT_ITEM_ID] = i
		}
	}

	override fun getItemViewType(position: Int): Int {
		return if (items[position].event != null) {
			0
		} else {
			1
		}
	}

	override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
		if (viewType == 0) {
			val rowView = inflater.inflate(R.layout.departure, parent, false)
			return BimbaDepartureViewHolder(rowView)
		} else {
			val rowView = inflater.inflate(R.layout.alert, parent, false)
			return BimbaAlertViewHolder(rowView)
		}
	}

	override fun onBindViewHolder(holder: ViewHolder, position: Int) {
		if (holder is BimbaDepartureViewHolder) {
			BimbaDepartureViewHolder.bind(
				items[position].event!!,
				holder,
				context,
				showAsTime,
				onClickListener,
				showingTerminusArrivals
			)
		} else {
			BimbaAlertViewHolder.bind(items[position].alert, holder as BimbaAlertViewHolder, context)
		}
	}

	override fun getItemCount(): Int = items.size

	fun get(id: String): EventItem? {
		val position = departuresPositions[id]
		return if (position == null) {
			null
		} else {
			items[position]
		}
	}

	fun update(
		departures: List<EventItem>,
		showAsTime: Boolean,
		areNewObserved: Boolean = false,
		leaveAlert: Boolean = false
	) {
		val newDepartures = if (leaveAlert && items.getOrNull(0)?.alert?.isNotEmpty() == true) {
			listOf(items[0]) + departures
		} else {
			departures
		}
		val newPositions: MutableMap<String, Int> = HashMap()
		newDepartures.forEachIndexed { i, departure ->
			newPositions[departure.event?.id ?: ALERT_ITEM_ID] = i
		}
		val diff = DiffUtil.calculateDiff(
			DiffUtilCallback(
				this.items,
				newDepartures,
				this.showAsTime != showAsTime
			)
		)

		this.showAsTime = showAsTime

		this.items = newDepartures
		departuresPositions = newPositions
		if (areNewObserved) {
			lastUpdate = ZonedDateTime.now()
		}
		diff.dispatchUpdatesTo(this)
	}

	fun refreshItems() {
		update(this.items, showAsTime)
	}
}

class DepartureBottomSheet(private var event: Event) : BottomSheetDialogFragment() {
	companion object {
		const val TAG = "DepartureBottomSheet"
	}

	private var cancelCallback: (() -> Unit)? = null

	fun setOnCancel(callback: () -> Unit) {
		cancelCallback = callback
	}

	override fun onCancel(dialog: DialogInterface) {
		super.onCancel(dialog)
		cancelCallback?.let { it() }
	}

	fun departureID(): String {
		return event.id
	}

	fun update(event: Event) {
		this.event = event
		view?.let { context?.let { ctx -> setContent(it, ctx, true) } }
	}

	private fun setContent(view: View, ctx: Context, updating: Boolean = false) {
		view.apply {
			val arrivalStatus = findViewById<TextView>(R.id.arrival_status)
			val arrivalTime = findViewById<TextView>(R.id.arrival_time)
			val departureStatus = findViewById<TextView>(R.id.departure_status)
			val departureTime = findViewById<TextView>(R.id.departure_time)
			if (event.arrivalTime == event.departureTime) {
				if (!event.exact) {
					arrivalStatus.apply {
						visibility = View.VISIBLE
						text = context.getString(R.string.approximately)
					}
				} else {
					arrivalStatus.visibility = View.GONE
				}
				findViewById<TextView>(R.id.arrival_time).apply {
					text = event.arrivalTimeString(ctx)
					visibility = View.VISIBLE
				}
				departureStatus.visibility = View.GONE
				departureTime.visibility = View.GONE
			} else {
				if (event.arrivalTime != null) {
					arrivalTime.visibility = View.VISIBLE
					arrivalTime.text = event.arrivalTimeString(ctx)
					arrivalStatus.visibility = View.VISIBLE
					arrivalStatus.text = if (!event.exact) {
						context?.getString(R.string.arrival_approximate)
					} else {
						context?.getString(R.string.arrival)
					}
				}
				if (event.departureTime != null) {
					departureTime.visibility = View.VISIBLE
					departureTime.text = event.departureTimeString(ctx)
					departureStatus.visibility = View.VISIBLE
					departureStatus.text = if (!event.exact) {
						context?.getString(R.string.departure_approximate)
					} else {
						context?.getString(R.string.departure)
					}
				}
			}
			findViewById<TextView>(R.id.local_time).visibility =
				if (event.timeZone() == ZoneId.systemDefault().id) {
					View.GONE
				} else {
					View.VISIBLE
				}

			findViewById<ImageView>(R.id.rt_icon).apply {
				visibility = if (event.isRealtime) {
					View.VISIBLE
				} else {
					View.GONE
				}
			}
			findViewById<ImageView>(R.id.wheelchair_icon).apply {
				visibility = if (event.vehicle.let {
						it.getCapability(Vehicle.Capability.LOW_FLOOR) || it.getCapability(Vehicle.Capability.LOW_ENTRY) || it.getCapability(
							Vehicle.Capability.RAMP
						)
					}) {
					View.VISIBLE
				} else {
					View.GONE
				}
			}

			findViewById<TextView>(R.id.line).apply {
				contentDescription = if (event.vehicle.Headsign.isNotBlank()) {
					getString(
						R.string.vehicle_headsign_content_description,
						event.vehicle.Line.name,
						event.vehicle.Headsign
					)
				} else {
					getString(
						R.string.vehicle_headsign_content_description_no_headsign,
						event.vehicle.Line.name,
					)
				}
				text = if (event.vehicle.Headsign.isNotBlank()) {
					getString(
						R.string.vehicle_headsign,
						event.vehicle.Line.name,
						event.vehicle.Headsign
					)
				} else {
					getString(
						R.string.vehicle_headsign_no_headsign,
						event.vehicle.Line.name,
					)
				}
			}


			event.boardingText(ctx).let {
				findViewById<TextView>(R.id.boarding_text).text = it
				findViewById<ImageView>(R.id.boarding_icon).visibility = if (it == "") {
					View.GONE
				} else {
					View.VISIBLE
				}
			}
			UnitSystem.getSelected(requireContext()).let { us ->
				findViewById<TextView>(R.id.speed_text).apply {
					text =
						us.toString(context, us.speedUnit(event.vehicle.Speed))
					contentDescription =
						us.speedUnit(event.vehicle.Speed).contentDescription(requireContext(), us.base)
				}
			}

			findViewById<LinearLayout>(R.id.congestion).visibility =
				if (event.vehicle.congestionLevel == CongestionLevel.UNKNOWN) View.GONE else View.VISIBLE
			findViewById<TextView>(R.id.congestion_text).text = event.vehicle.congestion(ctx)

			findViewById<LinearLayout>(R.id.occupancy).visibility =
				if (event.vehicle.occupancyStatus == OccupancyStatus.UNKNOWN) View.GONE else View.VISIBLE
			findViewById<TextView>(R.id.occupancy_text).text = event.vehicle.occupancy(ctx)

			findViewById<ImageView>(R.id.ac).let {
				TooltipCompat.setTooltipText(
					it,
					getString(R.string.air_condition_content_description)
				)
				it.visibility =
					if (event.vehicle.getCapability(Vehicle.Capability.AC)) View.VISIBLE else View.GONE
			}

			findViewById<ImageView>(R.id.bike).let {
				TooltipCompat.setTooltipText(
					it,
					getString(R.string.bicycles_allowed_content_description)
				)
				it.visibility =
					if (event.vehicle.getCapability(Vehicle.Capability.BIKE)) {
						View.VISIBLE
					} else {
						View.GONE
					}
			}

			findViewById<ImageView>(R.id.voice).let {
				TooltipCompat.setTooltipText(
					it,
					getString(R.string.voice_announcements_content_description)
				)
				it.visibility =
					if (event.vehicle.getCapability(Vehicle.Capability.VOICE)) {
						View.VISIBLE
					} else {
						View.GONE
					}
			}
			findViewById<ImageView>(R.id.ticket).let { ticketImage ->
				TooltipCompat.setTooltipText(
					ticketImage,
					getString(R.string.tickets_sold_content_description)
				)
				ticketImage.visibility = if (event.vehicle.let {
						it.getCapability(Vehicle.Capability.TICKET_DRIVER) || it.getCapability(Vehicle.Capability.TICKET_MACHINE)
					}) {
					View.VISIBLE
				} else {
					View.GONE
				}
			}
			findViewById<ImageView>(R.id.usb).let {
				TooltipCompat.setTooltipText(
					it,
					getString(R.string.usb_charging_content_description)
				)
				it.visibility =
					if (event.vehicle.getCapability(Vehicle.Capability.USB_CHARGING)) {
						View.VISIBLE
					} else {
						View.GONE
					}
			}

			if (event.alerts.isNotEmpty()) {
				findViewById<MaterialTextView>(R.id.alerts_text).text = event.alerts.map {
					it.header.ifEmpty {
						getString(R.string.alert_header)
					}
				}.toSet().joinToString(separator = "\n")
				findViewById<LinearLayout>(R.id.alerts).apply {
					visibility = View.VISIBLE
					setOnClickListener {
						MaterialAlertDialogBuilder(context)
							.setTitle(R.string.alerts)
							.setPositiveButton(R.string.ok) { _, _ -> }
							.setMessage(event.alerts.map { it.description }.filter { it != "" }
								.joinToString(separator = "\n"))
							.show()
					}
				}
			}

			findViewById<MapView>(R.id.map).let { map ->
				if (event.vehicle.Position.isZero()) {
					map.visibility = View.GONE
					return@let
				}
				map.controller.apply {
					GeoPoint(
						event.vehicle.location().positionLatitude,
						event.vehicle.location().positionLongitude
					).let { geoPoint ->
						if (updating) {
							animateTo(
								geoPoint, 19.0f.toDouble(), 3 * 1000
							)
						} else {
							setCenter(geoPoint)
							setZoom(19f.toDouble())
						}
					}
				}

				map.overlays.removeAll { marker ->
					marker is Marker
				}
				val marker = Marker(map).apply {
					position =
						GeoPoint(
							event.vehicle.location().positionLatitude,
							event.vehicle.location().positionLongitude
						)
					setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
					icon = context?.let { ctx -> event.vehicle.icon(ctx, 2f) }
					setOnClickListener {}
				}
				map.overlays.add(marker)
				map.invalidate()
			}
		}
	}

	@SuppressLint("ClickableViewAccessibility")
	override fun onCreateView(
		inflater: LayoutInflater,
		container: ViewGroup?,
		savedInstanceState: Bundle?
	): View {
		val content = inflater.inflate(R.layout.departure_bottom_sheet, container, false)

		context?.let { ctx ->
			content.apply {
				findViewById<MapView>(R.id.map).let { map ->
					map.setTileSource(TileSourceFactory.MAPNIK)
					if (((context?.resources?.configuration?.uiMode ?: UI_MODE_NIGHT_UNDEFINED)
							and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES
					) {
						map.overlayManager.tilesOverlay.setColorFilter(TilesOverlay.INVERT_COLORS)
					}
					map.zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
					map.setOnTouchListener { _, _ -> true }
					map.setMultiTouchControls(true)
					map.overlays.add(RotationGestureOverlay(map).apply { isEnabled = true })
				}

				setContent(this, ctx)

				(dialog as BottomSheetDialog).behavior.peekHeight = dpToPixelI(180f)

			}
		}
		return content
	}
}
